<?php
/**
 * <b>NOTE:</b>
 * <pre>
 *     1. current package max volume = maxPackageCount + purchasedVolume
 *     2. structure of $this->list is:
 *         array(
 *             itemDefId => array(
 *                 itemId => ModuleItemVo,
 *                 ...
 *             ),
 *             ...
 *         )
 * </pre>
 */
class ModuleItemVoList extends SzAbstractMySqlVoList
{

    /**
     * Total item grid count admitted in the user package, this value is the data designed in config file.
     * If the value of this param is 0, means there is no package grid count limit in the system.
     *
     * @var int
     */
    protected $maxPackageCount = null;
    /**
     * Item package grid count purchased by user.
     *
     * @var int
     */
    protected $purchasedVolume = null;
    /**
     * Total item grid count occupied in the user package.<br/>
     * <b>NOTE:</b> This is the grid number, not the item number.
     *
     * @var int
     */
    protected $curTotallyOccupiedGridCount = null;
    /**
     * Total item count of a itemDefId.<br/>
     * Since current list stored in this class is $this->list[itemDefId][itemId],<br/>
     * we need a simple way to retrieve item count according to a itemDefId.
     *
     * @var array
     */
    protected $itemDefCount = array();

    /**
     * Now timestamp.
     *
     * @var int
     */
    protected $now;
    /**
     * Used to get itemDefId when system knows only the itemId.
     * array(
     *     itemId => itemDefId,
     *     ...
     * )
     *
     * @var array
     */
    protected $itemDefIndex = array();
    /**
     * The length of $this->insertList before any manually action.<br/>
     * It would be logged when function $this->prepareForManuallyMultiInsert is called.<br/>
     * And cleared when function $this->getLastInsertedItems is called.
     *
     * @var int
     */
    protected $insertListLengthBeforeManuallyAction = null;
    /**
     * The player userId.
     *
     * @var int
     */
    protected $userId = null;
    /**
     * The exceeded items.<br/>
     * Records the exceeded items when package is full and exceeded items shall be handled later with some app logic.<br/>
     *
     * <pre>
     * The structure of this array is list below, it's designed to record items according to action sequence,
     * rather than the structure of items grouped according to $itemDefId.
     *
     * array(
     *      array($itemDefId => $count),
     *      ...
     * )
     * </pre>
     */
    protected $exceededList = array();
    /**
     * The length of $this->exceededList before any next action.<br/>
     * It would be logged when function $this->prepareForNextExceededItems is called.<br/>
     * And cleared when function $this->getLastExceededItems is called.
     *
     * @var int
     */
    protected $exceededListLengthAfterLastAction = null;

    /**
     * Initialize ModuleItemVoList.
     *
     * @param array $list array of ModuleItemVo
     * @return ModuleItemVoList
     */
    public function __construct($list)
    {
        parent::__construct($list);

        $this->listClassName = 'ModuleItemVoList';
        $this->ormName = 'ModuleItem';

        // initialize timestamp
        $this->now = SzTime::getTime();

        // initialize max package count
        $config = SzConfig::get()->loadAppConfig(ModuleItemConst::CONFIG_ITEM, ModuleItemConst::CONFIG_KEY_MAX_BAG_COUNT, true);
        $this->maxPackageCount = $config['value']; // array('key' => string, 'value' => int)

        if ($this->userId) {
            $this->initializePackageVolume($this->userId);
        }
    }

    //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
    //-* API FUNCTIONS
    //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
    /**
     * Get one element from the list.
     *
     * @param int $itemId
     * @param boolean $errNotFound default true, whether throw exception when element not found
     * @throws SzException 10511
     * @return ModuleItemVo $element
     */
    public function getElement($itemId, $errNotFound = true)
    {
        if (!SzUtility::checkArrayKey($itemId, $this->itemDefIndex) && $errNotFound) {
            throw new SzException(10511, array($itemId, get_class($this) . '::$itemDefIndex'));
        } else {
            $itemDefId = $this->itemDefIndex[$itemId];
        }
        if (!SzUtility::checkArrayKey($itemId, $this->list[$itemDefId]) && $errNotFound) {
            throw new SzException(10511, array($itemId, get_class($this) . '::$list[' . $itemDefId . ']'));
        }
        return $this->list[$itemDefId][$itemId];
    }

    /**
     * @see SzAbstractVoList::addElement
     * @param ModuleItemVo $element
     * @param boolean $allowExceed default false
     * <pre>
     *     TRUE: if item count exceed the max count of the user package, system will not throw exception
     * </pre>
     * @throws SzException 10518, 30112
     * @return void
     */
    public function addElement($element, $allowExceed = false)
    {
        if (!($element instanceof SzAbstractVo)) {
            throw new SzException(10518);
        }
        /* @var ModuleItemVo $element */
        if ($this->maxPackageCount // package count has limit
            && ($this->getMaxPackageCount($element->getUserId()) < ($this->curTotallyOccupiedGridCount + 1))
        ) { // item count exceed the max count admitted
            if ($allowExceed) {
                // recorded exceeded items
                array_push($this->exceededList, array($element->getItemDefId() => $element->getCount()));
                return; // do not throw exception
            } else {
                throw new SzException(30112, null, ModuleItemConst::MODULE_NAME);
            }
        }

        // commodity change
        if ($element->getExpireTime() > 0) {
            $now = SzSystem::getReqTime();
            $expireTime = ($element->getExpireTime() == ModuleItemConst::PERMANENTLY_ITEM_EXPIRE_TIME) ? $now + SzTime::HOURS24 * 180 : $element->getExpireTime();

            /**
             * @var ModuleItemDurationLogVoList $itemDurationList
             * @var ModuleItemDurationLogVo $itemDuration
             */
            $itemDurationList = SzPersister::get()->getVoList($element->getUserId(), ModuleItemConst::ORM_NAME_ITEM_DURATION_LOG);
            $itemDuration = new ModuleItemDurationLogVo(
                null, // id
                $element->getUserId(), // uid
                $element->getItemDefId(), // itemid
                $now, // c_time
                $expireTime, // expire_time
                $element->getCommodityValue(), // c_value
                true
            );
            $itemDurationList->addElement($itemDuration);
            SzPersister::get()->setVoList($itemDurationList);

            // clear
            $element->setCommodityValue(0);
        }

        // update insert list
        $this->insertList[] = $element;

        // update package grid count
        ++$this->curTotallyOccupiedGridCount;
        // update item count of itemDefId
        $this->updateItemCountInInsertList($element);
    }

    /**
     * Sometimes we have to change the item count of the elements in insertList.
     * And we need also update the count of the itemDefId in "$this->itemDefCount".
     *
     * <pre>
     * If this function is called in addElement, then the second param is no necessary.
     * Since the total count of the element shall all be added into "$this->itemDefCount".
     * Otherwise, the second param have to be provided, for the diff change.
     * </pre>
     *
     * @param ModuleItemVo $element
     * @param int $count default null
     * @return void
     */
    public function updateItemCountInInsertList($element, $count = null)
    {
        if (!SzUtility::checkArrayKey($element->getItemDefId(), $this->itemDefCount)) {
            $this->itemDefCount[$element->getItemDefId()] = 0;
        }
        if (!is_null($count)) {
            $this->itemDefCount[$element->getItemDefId()] += $count;
        } else {
            $this->itemDefCount[$element->getItemDefId()] += $element->getCount();
        }
    }

    /**
     * @see SzAbstractVoList::updateElement()
     * @see ModuleItemVoList::setElement()
     * @param ModuleItemVo $element
     * @param boolean $allowExceed default false
     * <pre>
     *     TRUE: if item count exceed the max count of the user package, system will not throw exception
     * </pre>
     * @param boolean $errNotFound default true, whether throw exception when element not found
     * @param boolean $addToUpdateList default true
     * <pre>
     *     Sometimes this function is called in ModuleItemVoList::setElement,
     *     and in that case, persistence shall already have been done,
     *     and we don't need to do update action again.
     *     This switch is design to satisfy the requirement.
     * </pre>
     * @throws SzException 10518, 30101, 30112
     * @return void
     */
    public function updateElement($element, $allowExceed = false, $errNotFound = true, $addToUpdateList = true)
    {
        if (!($element instanceof SzAbstractVo)) {
            throw new SzException(10518);
        }
        /* @var ModuleItemVo $element */
        $itemId = $element->getItemId();
        $itemDefId = $element->getItemDefId();
        if ((!SzUtility::checkArrayKey($itemDefId, $this->list) || !SzUtility::checkArrayKey($itemId, $this->list[$itemDefId])) && $errNotFound) {
            throw new SzException(30101, array($itemId), ModuleItemConst::MODULE_NAME);
        }
        $userId = $element->getUserId();

        // check changed item count
        if ($this->maxPackageCount) {
            if ($element->getPrevCount() != 0 && $element->getCount() == 0) { // count changed from not 0 to 0
                --$this->curTotallyOccupiedGridCount;
            } else if ($element->getPrevCount() == 0 && $element->getCount() != 0) { // count changed from 0 to not 0
                if ($this->getMaxPackageCount($userId) < $this->curTotallyOccupiedGridCount + 1) { // item count exceed the max count admitted
                    if ($allowExceed) {
                        // recorded exceeded items
                        array_push($this->exceededList, array($itemDefId => $element->getCount()));
                        return; // do not throw exception
                    } else {
                        throw new SzException(30112, null, ModuleItemConst::MODULE_NAME);
                    }
                }
                // update package grid count
                ++$this->curTotallyOccupiedGridCount;
            }
        }
        // sync current item count to temporarily count
        $element->syncPrevCount();

        // commodity change
        if ($element->getExpireTime() > 0) {
            ModuleItem::get()->addModuleItemDurationLog($element->getUserId(), $element->getItemDefId(), $element->getExpireTime(), $element->getCommodityValue());
            $element->setCommodityValue(0);
        }

        // update updateList
        if ($addToUpdateList) {
            $this->updateList[$itemId] = $element;
        }
        // update item list
        $this->list[$itemDefId][$itemId] = $element;

        // recount item count
        $countDelta = 0;
        /* @var ModuleItemVo $item */
        foreach ($this->list[$itemDefId] as $item) {
            $countDelta += $item->getCount();
        }

        // update item count of itemDefId
        if (!SzUtility::checkArrayKey($itemDefId, $this->itemDefCount)) {
            $this->itemDefCount[$itemDefId] = 0;
        }
        $this->itemDefCount[$itemDefId] = $countDelta;
    }

    /**
     * @see SzAbstractVoList::deleteElement()
     * @param int $itemId
     * @param boolean $errNotFound default true, whether throw exception when element not found
     * @throws SzException 30101
     * @return void
     */
    public function deleteElement($itemId, $errNotFound = true)
    {
        $itemDefId = $this->itemDefIndex[$itemId];
        if (!$this->list[$itemDefId][$itemId] && $errNotFound) {
            throw new SzException(30101, array($itemId), ModuleItemConst::MODULE_NAME);
        }
        /* @var ModuleItemVo $deleteElement */
        $deleteElement = $this->list[$itemDefId][$itemId];
        // update deleteList
        $this->deleteList[$itemId] = $deleteElement;
        // update item list
        unset($this->list[$itemDefId][$itemId]);
        unset($this->itemDefIndex[$itemId]);
        // update package grid count
        --$this->curTotallyOccupiedGridCount;
        // update item count of itemDefId
        if (!SzUtility::checkArrayKey($itemDefId, $this->itemDefCount)) {
            $this->itemDefCount[$itemDefId] = 0;
        }
        $this->itemDefCount[$itemDefId] -= $deleteElement->getCount();
        // update item list, remove itemDef structures if no items exist in this structure
        if ($this->itemDefCount[$itemDefId] == 0
            && count($this->list[$itemDefId]) == 0) {
            unset($this->list[$itemDefId]);
            unset($this->itemDefCount[$itemDefId]);
        }
    }

    /**
     * @see SzAbstractVoList::setElement
     * @param int $id
     * @param ModuleItemVo $element
     * @throws SzException 10518
     * @return void
     */
    public function setElement($id, $element)
    {
        if (!($element instanceof SzAbstractVo)) {
            throw new SzException(10518);
        }
        /* @var ModuleItemVo $element */
        $itemId = $element->getItemId();
        $itemDefId = $element->getItemDefId();
        $elementAlreadyExist = isset($this->list[$itemDefId][$itemId]);

        if ($elementAlreadyExist) {
            // update
            $this->updateElement($element, false, true, false);
        } else {
            // insert
            $this->addElement($element, false, false);
        }
    }

    /**
     * Set some manually inserted elements into $this->list.
     * Also update some global counts, like max package count.
     *
     * @param ModuleItemVo $element
     * @throws SzException 10518, 30112
     * @return void
     */
    public function setManuallyInsertedElement($element)
    {
        if (!($element instanceof SzAbstractVo)) {
            throw new SzException(10518);
        }
        /* @var ModuleItemVo $element */
        $itemId = $element->getItemId();
        $itemDefId = $element->getItemDefId();

        // need to update package grid count
        if ($this->maxPackageCount) {
            $userId = $element->getUserId();
            if ($this->getMaxPackageCount($userId) < $this->curTotallyOccupiedGridCount + 1) { // item count exceed the max count admitted
                throw new SzException(30112, null, ModuleItemConst::MODULE_NAME);
            }
            // update package grid count
            ++$this->curTotallyOccupiedGridCount;
        }

        // update item list
        $this->list[$itemDefId][$itemId] = $element;

        // recount item count
        $countDelta = 0;
        /* @var ModuleItemVo $item */
        foreach ($this->list[$itemDefId] as $item) {
            $countDelta += $item->getCount();
        }

        // update item count of itemDefId
        if (!SzUtility::checkArrayKey($itemDefId, $this->itemDefCount)) {
            $this->itemDefCount[$itemDefId] = 0;
        }
        $this->itemDefCount[$itemDefId] = $countDelta;
    }

    /**
     * Record the length of $this->insertList before any manually action.<br/>
     * If $this->insertListLengthBeforeManuallyAction was not null, when the function was called,
     * exception 30120 would be thrown.
     *
     * <pre>
     * Use this series function for only not stackable item insert actions.
     * </pre>
     *
     * @throws SzException 30120
     * @return void
     */
    public function prepareForManuallyMultiInsert()
    {
        if (!is_null($this->insertListLengthBeforeManuallyAction)) { // it shall be null here
            throw new SzException(30120, null, ModuleItemConst::MODULE_NAME);
        }

        $this->insertListLengthBeforeManuallyAction = count($this->insertList);
    }

    /**
     * Get the array of ModuleItemVo added into the $this->insertList, after function $this->prepareForManuallyMultiInsert is called.<br/>
     * Note all the ModuleItemVo got are all removed from the $this->insertList,
     * since <a href="http://php.net/manual/en/function.array-splice.php">array_splice</a> is used.
     *
     * <pre>
     * Use this series function for only not stackable item insert actions.
     * </pre>
     *
     * @return array
     * <pre>
     * array(ModuleItemVo, ...)
     * </pre>
     */
    public function getLastInsertedItems()
    {
        $items = array_splice($this->insertList, $this->insertListLengthBeforeManuallyAction);
        $this->insertListLengthBeforeManuallyAction = null;

        /**
         * Previously we use $this->addElement added records into list, grid occupied count had been added in $this->addElement.
         * And later we will use $this->setManuallyInsertedElement to add manually added records into list,
         * and those manually added records are all grabbed from this function ($this->getLastInsertedItems),
         * so their grid rooms shall been released now, since they are array_splice from $this->list, no longer occupy grid rooms.
         */
        $this->curTotallyOccupiedGridCount -= count($items);

        return $items;
    }

    /**
     * Record the length of $this->exceededList action.<br/>
     * If $this->exceededListLengthAfterLastAction was not null, exception 30121 would be thrown.
     *
     * <pre>
     * The function pair of
     * ModuleItemVoList::prepareForNextExceededItems
     * &&
     * ModuleItemVoList::getLastExceededItems
     * are designed for the requirement:
     *
     * In the multi package request mode, we may need to separate exceeded items into several batch according to corresponding requests.
     * Thus the app logic can do specified actions to those exceed items according to the requests.
     * So we need to do ModuleItemVoList::prepareForNextExceededItems beforehand, then when one of the package request done,
     * we can do ModuleItemVoList::getLastExceededItems to get the exceeded items in this request.
     * </pre>
     *
     * @see ModuleItemVoList::getLastExceededItems
     *
     * @throws SzException 30121
     * @return void
     */
    public function prepareForNextExceededItems()
    {
        if (!is_null($this->exceededListLengthAfterLastAction)) { // it shall be null here
            throw new SzException(30121, null, ModuleItemConst::MODULE_NAME);
        }

        $this->exceededListLengthAfterLastAction = count($this->exceededList);
    }

    /**
     * Get the array of ModuleItemVo added into the $this->exceededList, after function $this->prepareForNextExceededItems is called.<br/>
     * Note all the ModuleItemVo got are all removed from the $this->exceededList,
     * since <a href="http://php.net/manual/en/function.array-splice.php">array_splice</a> is used.
     *
     * @return array
     * <pre>
     * array(
     *      array($itemDefId => count),
     *      ...
     * )
     * </pre>
     */
    public function getLastExceededItems()
    {
        $exceededItems = array_splice($this->exceededList, $this->exceededListLengthAfterLastAction);
        $this->exceededListLengthAfterLastAction = null;

        return $exceededItems;
    }

    /**
     * Remove all items in insertList via itemDefId.
     *
     * @param int $itemDefId
     * @return void
     */
    public function removeItemsInInsertListViaDefId($itemDefId)
    {
        foreach ($this->insertList as $index => $item) {
            /* @var ModuleItemVo $item */
            if ($item->getItemDefId() == $itemDefId) {
                array_splice($this->insertList, $index, 1);
            }
        }
    }

    /**
     * Remove specified item in insertList.
     *
     * @param ModuleItemVo $item
     * @return void
     */
    public function removeItemInInsertList($item)
    {
        foreach ($this->insertList as $index => $insertItem) {
            /* @var ModuleItemVo $insertItem */
            if ($item->getItemDefId() == $insertItem->getItemDefId() && $item->getCount() == $insertItem->getCount()) {
                array_splice($this->insertList, $index, 1);
            }
        }
    }

    //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
    //-* UTILITIES FUNCTIONS
    //-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-
    /**
     * Reformat the item list.
     * <pre>
     *     1. from array(itemId => ModuleItemVo) to array(itemDefId => array(itemId => ModuleItemVo))
     *     2. count how many grids are occupied($this->curTotallyOccupiedGridCount)
     *     3. sort items under each itemDefId into ASC ordered
     * </pre>
     *
     * @see SzAbstractVoList::formatList
     * @param array $list
     * @return array $list
     */
    protected function formatList($list)
    {
        if ($list) {
            $this->curTotallyOccupiedGridCount = 0;

            // initialize $this->userId
            /* @var ModuleItemVo $firstItem */
            $firstItem = current($list);
            $this->userId = $firstItem->getUserId();

            // reformat list
            $formatted = array();
            foreach ($list as $itemId => $item) {
                /* @var ModuleItemVo $item */
                $itemDefId = $item->getItemDefId();
                $formatted[$itemDefId][$itemId] = $item;
                // package grid data
                if ($item->getCount() > 0 // item count bigger than 0
                    || $this->now < $item->getExpireTime()) { // item not expired yet
                    // means this item occupies package room
                    ++$this->curTotallyOccupiedGridCount;
                }
                // item count data
                if (!SzUtility::checkArrayKey($itemDefId, $this->itemDefCount)) {
                    $this->itemDefCount[$itemDefId] = 0;
                }
                if ($item->getCount() > 0 && is_null($item->getExpireTime())) {
                    $this->itemDefCount[$itemDefId] += $item->getCount();
                }
                // item reversed index
                $this->itemDefIndex[$itemId] = $itemDefId;
            }
            $list = $formatted;

            // sort list
            foreach ($list as $itemDefId => $items) { // ITEM_DEF_ID level
                uasort($items, array('ModuleItemVoList', 'sortByItemCount'));
                $list[$itemDefId] = $items;
            }
        } else {
            $this->curTotallyOccupiedGridCount = 0;
            $this->itemDefIndex = array();
        }

        return $list;
    }

    /**
     * @see SzAbstractVoList::searchElementExists
     * @param ModuleItemVo $element
     * @param SzAbstractModel $model
     * @param string $pkValue
     * @return boolean true means exists, false means not
     */
    protected function searchElementExists($element, $model, $pkValue = null)
    {
        if (!SzUtility::checkArrayKey($element->getItemDefId(), $this->list)) {
            return false;
        } else if (!SzUtility::checkArrayKey($element->getItemId(), $this->list[$element->getItemDefId()])) {
            return false;
        }

        return true;
    }

    /**
     * @see SzAbstractVoList::setVoInList
     * @param ModuleItemVo $element
     * @param SzAbstractModel $model
     * @param string $pkValue
     * @return void
     */
    protected function setVoInList($element, $model, $pkValue = null)
    {
        $this->list[$element->getItemDefId()][$element->getItemId()] = $element;
    }

    /**
     * @see SzAbstractVoList::toArray
     * @return array
     */
    public function toArray()
    {
        $result = array();

        if ($this->list) {
            foreach ($this->list as $itemDefId => $items) {
                if ($items) {
                    foreach ($items as $itemId => $item) {
                        /* @var ModuleItemVo $item */
                        $result[$itemDefId][$itemId] = $item->toArray();
                    }
                }
            }
        }

        return $result;
    }

    /**
     * Compare two ModuleItemVo via item count, ASC.
     *
     * @param ModuleItemVo $itemA
     * @param ModuleItemVo $itemB
     * @return int -1 | 0 | 1
     */
    protected function sortByItemCount($itemA, $itemB)
    {
        $result = 0;
        if ($itemA->getCount() == $itemB->getCount()) {
            $result = 0;
        } else if ($itemA->getCount() > $itemB->getCount()) {
            $result = 1;
        } else if ($itemA->getCount() < $itemB->getCount()) {
            $result = -1;
        }
        return $result;
    }

    /**
     * Get elements from the list[itemDefId] via itemDefId.
     *
     * @param int $itemDefId
     * @return array | null
     */
    public function getElementsByItemDefId($itemDefId)
    {
        return isset($this->list[$itemDefId]) ? $this->list[$itemDefId] : null;
    }

    /**
     * Get first element from the list[itemDefId] via itemDefId.
     *
     * @param int $itemDefId
     * @return ModuleItemVo | null
     */
    public function getFirstElementByItemDefId($itemDefId)
    {
        return isset($this->list[$itemDefId]) ? array_shift($this->list[$itemDefId]) : null;
    }

    /**
     * Set purchased package volume.
     *
     * @param int $volume
     * @return void
     */
    public function setPurchasedVolume($volume)
    {
        $this->purchasedVolume = $volume;
    }

    /**
     * Get the max package volume count.
     * Including user purchased package volume.
     *
     * <pre>
     * <b>NOTE:</b>
     *     This function is the main function of the package volume logic,
     *     since add item & update item function will check this function at the beginning
     *     to ensure there is enough room for the new items.
     *     Thus we will check the package volume status initialized or not in this function,
     *     if false we will init the status first.
     * <b>WHY:</b>
     *     We cannot do it in __construct, since at that time there is no $userId provided,
     *     and we cannot get the purchased volume count
     * </pre>
     *
     * @param int $userId
     * @return int $maxPackageCount
     */
    public function getMaxPackageCount($userId)
    {
        $this->initializePackageVolume($userId);
        return $this->maxPackageCount + $this->purchasedVolume;
    }

    /**
     * Check the $this->maxPackageCount first:
     * <pre>
     *     1. null: no volume limit, return false
     *     2. number: there is the package volume limit in the system
     *         * check $this->purchasedVolume
     *         * null means not initialized yet, do initialization
     *         * return true
     * </pre>
     *
     * @param int $userId
     * @return void
     */
    protected function initializePackageVolume($userId)
    {
        if (!$this->userId) {
            $this->userId = $userId;
        }
        if ($this->maxPackageCount // there is the package volume limit in the system
            && is_null($this->purchasedVolume) // purchased package volume not initialized
        ) {
            // set purchased package volume
            /* @var ModuleItemVolumeVo $volume */
            $volume = SzPersister::get()->getVo($userId, ModuleItemConst::ORM_NAME_ITEM_VOLUME);
            if ($volume) {
                // gridId + 1 means volume, since gridId starts from 0
                $this->setPurchasedVolume($volume->getGridId() + 1);
            } else {
                // volume object never created yet
                SzPersister::get()->setVo(new ModuleItemVolumeVo($userId, -1, true));
                $this->setPurchasedVolume(0);
            }
        }
    }

    /**
     * Add count one of $this->curTotallyOccupiedCount,
     *
     * @return void
     */
    protected function addCurTotallyOccupiedCount()
    {
        ++$this->curTotallyOccupiedGridCount;
    }

    /**
     * Reduce count one of $this->curTotallyOccupiedCount,
     *
     * @return void
     */
    protected function reduceCurTotallyOccupiedCount()
    {
        --$this->curTotallyOccupiedGridCount;
    }

    /**
     * @see SzAbstractVoList::getListCount
     * @return int
     */
    public function getListCount()
    {
        return $this->curTotallyOccupiedGridCount;
    }

    /**
     * Get item count according to $itemDefId.
     *
     * @param int $itemDefId
     * @return int
     */
    public function getItemDefCount($itemDefId)
    {
        return (isset($this->itemDefCount[$itemDefId])) ? $this->itemDefCount[$itemDefId] : 0;
    }

    /**
     * Get item purchased count according to $itemDefId.
     *
     * @param int $itemDefId
     * @return array(int, int)
     */
    public function getPurchasedCountAndCommodityValueByDefId($itemDefId)
    {
        $items = $this->getElementsByItemDefId($itemDefId);
        $insertList = $this->getInsertList();
        foreach ($insertList as $insertItem) {
            /* @var ModuleItemVo $insertItem */
            if ($insertItem->getItemDefId() == $itemDefId) {
                if (is_null($items)) {
                    $items = array();
                }
                array_unshift($items, $insertItem);
            }
        }

        /** @var ModuleItemVo $item */
        $purchasedCount = 0;
        $commodityValue = 0;
        if ($items && is_array($items)) {
            foreach ($items as $item)
            {
                $purchasedCount += $item->getPurchasedCount();
                $commodityValue += $item->getCommodityValue();
            }
        }

        return array($purchasedCount, $commodityValue);
    }

    /**
     * @see SzAbstractVoList::persistInsertList
     */
    protected function persistInsertList($model, &$responses)
    {
        if (!$this->insertList) { // nothing to do with insert action
            return;
        }

        /* @var SzMySqlModel $model */
        if ($model->hasAutoIncrementId()) {
            // model has auto increment id
            if (count($this->insertList) > $this->batchInsertThreshold) {
                // use batch insert
                $dbResult = $model->insert($this->insertList);
                if ($dbResult[0] > 0) {
                    $shardKey = $this->getShardKeyFromList($model, $this->insertList);
                    if (!$shardKey) {
                        throw new SzException(10509, $model->getOrmName());
                    }

                    /**
                     * array(
                     *     $itemDefId => array(
                     *         $pk => ModuleItemVo,
                     *         ...
                     *     ),
                     *     ...
                     * )
                     * @var array $selectedList
                     * @see ModuleItemVoList::formatList
                     *
                     * The return value of ModuleItemModel::select is ModuleItemVoList,
                     * and the return value of ModuleItemVoList::getList is multi layer format,
                     * so the default SzAbstractMySqlVoList::persistInsertList won't work.
                     * We have to overwrite the function here.
                     */
                    $selectedList = $model->select($shardKey)->getList(); // select all to filter new inserted data
                    if (count($this->list) == 0) {
                        // empty list, set to cache & $this->list directly
                        foreach ($selectedList as $itemDefId => $vos) {
                            foreach ($vos as $vo) {
                                /* @var ModuleItemVo $vo */
                                $responses[] = $vo->buildResponse($model, true);
                                $this->setVoInList($vo, $model);
                            }
                        }
                        $model->setListOfVoCache($selectedList, $selectedList);
                    } else {
                        $newInserted = array();
                        foreach ($selectedList as $itemDefId => $vos) {
                            foreach ($vos as $vo) {
                                /* @var ModuleItemVo $vo */
                                if (!$this->searchElementExists($vo, $model, $itemDefId)) {
                                    $responses[] = $vo->buildResponse($model, true);
                                    $newInserted[] = $vo;
                                }
                                $this->setVoInList($vo, $model);
                            }
                        }
                        if ($newInserted) {
                            $model->setListOfVoCache($newInserted, $selectedList);
                        }
                    }
                }
            } else {
                // loop single insert
                foreach ($this->insertList as $vo) {
                    /* @var ModuleItemVo $vo */
                    $dbResult = $model->insert($vo);
                    if ($dbResult[0] > 0 && $dbResult[1]) {
                        $model->setColumnValue($vo, $model->getAutoIncrColumn(), $dbResult[1]);
                        $responses[] = $vo->buildResponse($model, true);
                        $vo->clearStatusChangeInfo();
                        $this->setVoInList($vo, $model);
                    }
                }
                $model->setListOfVoCache($this->insertList, $this->list);
            }
        } else {
            // model does not have auto increment id
            if (count($this->insertList) > $this->batchInsertThreshold) {
                // use batch insert
                $dbResult = $model->insert($this->insertList);
                if ($dbResult[0] > 0) {
                    foreach ($this->insertList as $vo) {
                        /* @var ModuleItemVo $vo */
                        $responses[] = $vo->buildResponse($model, true);
                        $vo->clearStatusChangeInfo();
                        $this->setVoInList($vo, $model);
                    }
                }
            } else {
                // loop single insert
                foreach ($this->insertList as $vo) {
                    /* @var ModuleItemVo $vo */
                    $dbResult = $model->insert($vo);
                    if ($dbResult[0] > 0) {
                        $responses[] = $vo->buildResponse($model, true);
                        $vo->clearStatusChangeInfo();
                        $this->setVoInList($vo, $model);
                    }
                }
            }
            $model->setListOfVoCache($this->insertList, $this->list);
        }

        $this->insertList = array();
    }

}